DynamoDBのLastEvaluatedKeyが示すレコードを削除した時の挙動について調べてみた
おつかれさまです。 最近、ドラマ「白い巨塔」を見て禁煙をはじめた新井@サーバーレス開発部です。
唐突ですが、みなさんはDynamoDBのLastEvaluatedKey
についてはご存知でしょうか?
※知らないという方はこちらをご覧ください。
簡単に言ってしまうと、DynamoDBのScanやQueryの操作で全件取得しきれなかった時に返却される、次点の開始位置を示すキーです。
DynamoDBのScanやQueryで全件取得しきれなかったり、最大取得件数(Limit
)を設定してベージネーションをしているときなど、よく見かける方も多いのではないでしょうか。
ちなみに、boto3のレスポンスだとこんな感じにLastEvaluatedKey
の情報が返却されます。
{ 'Items': [{'id': Decimal('1'), 'name': 'charlie'}], 'Count': 1, 'ScannedCount': 1, 'LastEvaluatedKey': {'id': Decimal('1'), 'name': 'charlie'} ...
で、今回は
「ページネーションを行っている間に、このキーが指すデータポイントが削除/更新された場合の挙動ってどうなるんだろう?」
「データ取得時にエラーになるのでは?」
「不整合がおきる?」
と気になったため調べてみた際のメモになります。
先に結論
結論から言うと、レスポンスデータが返されて次のリクエストを送るまでの間に、lastEvaluatedKey
が指しているデータが削除、変更されたとしてもなんら問題ありません。 (※というか、そもそも変更はできません。詳しくは後述)
lastEvaluatedKey
はデータそのものを参照しているのではなく、あくまで開始位置を示しているだけだからです。
データが削除されようが、更新されようが、前後に新しいデータが追加されようが、開始位置は変わりません。更新タイミングによってはデータの取得漏れがありますが、そこは割り切りですね。
やってみた
下記のテーブルに対し、Scan操作を実施していきます。
- テーブル名:
arai-test-table
- HashKey:
id
(Number) - RangeKey:
name
(String)
id | name |
---|---|
1 | alice |
1 | bob |
1 | charlie |
2 | dave |
2 | eve |
3 | frank |
Scanした時の挙動
- Python コード
import boto3 import time dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-1') table = dynamodb.Table('arai-test-table') last_key = None while True: params = {'Limit':1} if last_key: params['ExclusiveStartKey'] = last_key response = table.scan(**params) if 'LastEvaluatedKey' not in response: break last_key = response['LastEvaluatedKey'] print('====================ITEMS====================') print('Items', response['Items'], sep='=') print('LastEvaluatedKey', last_key, sep='=') time.sleep(3)
- 出力
python ddb_scan.py ====================ITEMS==================== Items=[{'id': Decimal('3'), 'name': 'frank'}] LastEvaluatedKey={'id': Decimal('3'), 'name': 'frank'} ====================ITEMS==================== Items=[{'id': Decimal('2'), 'name': 'dave'}] LastEvaluatedKey={'id': Decimal('2'), 'name': 'dave'} ====================ITEMS==================== Items=[{'id': Decimal('2'), 'name': 'eve'}] LastEvaluatedKey={'id': Decimal('2'), 'name': 'eve'} ====================ITEMS==================== Items=[{'id': Decimal('1'), 'name': 'alice'}] LastEvaluatedKey={'id': Decimal('1'), 'name': 'alice'} ====================ITEMS==================== Items=[{'id': Decimal('1'), 'name': 'bob'}] LastEvaluatedKey={'id': Decimal('1'), 'name': 'bob'} ====================ITEMS==================== Items=[{'id': Decimal('1'), 'name': 'charlie'}] LastEvaluatedKey={'id': Decimal('1'), 'name': 'charlie'}
Scanしたときの返却値の順番ってこんな感じになるんですね。。。
開始位置のItemを削除してみる
alice
まで読み込んだ後に、DynamoDBから{'id': Decimal('1'), 'name': 'alice'}
のレコードを削除してみます。
次点の検索では、ExclusiveStartKey
がalice
のレコードを指しています。
$ python ddb_scan.py ====================ITEMS==================== Items=[{'id': Decimal('3'), 'name': 'frank'}] LastEvaluatedKey={'id': Decimal('3'), 'name': 'frank'} ====================ITEMS==================== Items=[{'id': Decimal('2'), 'name': 'dave'}] LastEvaluatedKey={'id': Decimal('2'), 'name': 'dave'} ====================ITEMS==================== Items=[{'id': Decimal('2'), 'name': 'eve'}] LastEvaluatedKey={'id': Decimal('2'), 'name': 'eve'} ====================ITEMS==================== Items=[{'id': Decimal('1'), 'name': 'alice'}] LastEvaluatedKey={'id': Decimal('1'), 'name': 'alice'} ... # ここで、{id:1, name: alice}のレコードを削除 ... ====================ITEMS==================== Items=[{'id': Decimal('1'), 'name': 'bob'}] LastEvaluatedKey={'id': Decimal('1'), 'name': 'bob'} ====================ITEMS==================== Items=[{'id': Decimal('1'), 'name': 'charlie'}] LastEvaluatedKey={'id': Decimal('1'), 'name': 'charlie'}
なんら問題なく動作しました。
開始位置のItemを更新してみる
alice
まで読み込んだ後に、alice
の名前をarai
に変更して...
と考えていましたが、よく考えたらlastEvaluatedKey
にはユニークなプライマリーキーが返却されるので、プライマリーキーの変更はできません。
開始位置の前後にItem追加してみる
だいたい予想できるかとおもいますが、一応試してみます。
alice
まで読み込んだ後に、arai
とabe
というレコードを挿入してみます。
id | name |
---|---|
1 | abe |
1 | arai |
$ python ddb_scan.py ====================ITEMS==================== Items=[{'id': Decimal('3'), 'name': 'frank'}] LastEvaluatedKey={'id': Decimal('3'), 'name': 'frank'} ====================ITEMS==================== Items=[{'id': Decimal('2'), 'name': 'dave'}] LastEvaluatedKey={'id': Decimal('2'), 'name': 'dave'} ====================ITEMS==================== Items=[{'id': Decimal('2'), 'name': 'eve'}] LastEvaluatedKey={'id': Decimal('2'), 'name': 'eve'} ====================ITEMS==================== Items=[{'id': Decimal('1'), 'name': 'alice'}] LastEvaluatedKey={'id': Decimal('1'), 'name': 'alice'} ... # ここで、{id:1, name: abe}と{id:1, name: arai}のレコードを挿入 ... ====================ITEMS==================== Items=[{'id': Decimal('1'), 'name': 'arai'}] LastEvaluatedKey={'id': Decimal('1'), 'name': 'arai'} ====================ITEMS==================== Items=[{'id': Decimal('1'), 'name': 'bob'}] LastEvaluatedKey={'id': Decimal('1'), 'name': 'bob'} ====================ITEMS==================== Items=[{'id': Decimal('1'), 'name': 'charlie'}] LastEvaluatedKey={'id': Decimal('1'), 'name': 'charlie'}
ソート順的に、alice
の後に来るarai
の方のレコードは取得できていますね。
まとめ
LastEvaluatedKey
やExclusiveStartKey
はあくまで、開始位置や終了位置を示しているだけで、直接データを参照しているわけではないということですね。
開始位置にデータがあろうがなかろうが関係ないということです。
ちなみに、Queryでのテーブル操作の場合も挙動は同じでした!
よく考えてみれば当たり前なのですが、普段意識しない部分なので、動きが確認できてよかったです。あらめてDynamoDBとお近づきになれた気がします。
以上、どなたかの役に立てば幸いです。お疲れ様でした!
余談
AWS CLIから利用した場合は、LastEvaluatedKey
ではなくNextToken
が返却されました。
$ aws dynamodb scan \ --table-name arai-test-table \ --max-items 1 { "Items": [ { "id": { "N": "3" }, "name": { "S": "frank" } } ], "Count": 6, "ScannedCount": 6, "ConsumedCapacity": null, "NextToken": "eyJFeGNsdXNpdmVTdGFydEtleSI6IG51bGwsICJib3RvX3RydW5jYXRlX2Ftb3VudCI6IDF9" }
$ echo -n "eyJFeGNsdXNpdmVTdGFydEtleSI6IG51bGwsICJib3RvX3RydW5jYXRlX2Ftb3VudCI6IDF9" | base64 -d {"ExclusiveStartKey": null, "boto_truncate_amount": 1}
boto_truncate_amountこいつが何なのかちゃんと調べられていませんが、同じく開始位置を示しているっぽいのはわかります。ちなみにtrancated=「切り捨て」のことなので、普通に考えればoffset
のような扱いなんだと思います。
AWS CLIの場合は、コマンドに--starting-token
オプションを追加し、NextToken
の値をセットしてあげることで、途中からデータ取得を再開できます。